﻿using System;
using System.Collections.Generic;
using System.Text;
using ArlesInterfaces.Editor;
using ArlesInterfaces.Render;
using Arles.src.controller;
using Arles.src.controller.Commands;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using System.ComponentModel;
using System.Drawing.Design;
using Arles.src.view.controls;
using System.Windows.Forms;

namespace Arles.src.model.objects
{
    class SkydomeObject : ISceneObject, ITrackBarProperties
    {
        
        #region Private Members

        private GraphicsDevice graphics;
        private ContentManager content;

        private float fTheta;
        private float fPhi;

        private int texW;
        private int texH;

        private float previousTheta, previousPhi;

        private bool realTime;

        private ICamera camera;

        private Texture2D mieTex, rayleighTex;
        private RenderTarget2D mieRT, rayleighRT;
        private DepthStencilBuffer oldDepthBuffer, newDepthBuffer;

        private Texture2D moonTex, glowTex, starsTex, dummy;

        private Effect scatterEffect, texturedEffect;

        private SpriteBatch screenQuad;

        private VertexDeclaration vertexDecl;
        private VertexPositionTexture[] domeVerts, quadVerts;
        private short[] ib, quadIb;

        private int DomeN;
        private int DVSize;
        private int DISize;

        private Vector4 sunColor;

        private Vector4 lightDirection;
        private Vector4 lightColor;
        private Vector4 lightColorAmbient;
        //private float fDensity;
        private Vector3 waveLengths;
        private Vector3 invWaveLengths;
        private Vector3 waveLengthsMie;
        private int numSamples;
        private float exposure;

        Dictionary<string, TrackBarProperty> trackBarProperties;
        #endregion

        #region Properties

        [CategoryAttribute("Modo Simulación"), DescriptionAttribute("Establece si el Skydome simula el aspecto del cielo según la hora real.")]
        public bool RealTime
        {
            set 
            {
                this.realTime = value;
                UpdateMieRayleighTextures();
            }
            get { return this.realTime; }
        }

        [CategoryAttribute("Orientación"), DescriptionAttribute("Establece ángulo Theta (no tiene efecto si se activa RealTime).")]
        [Editor(typeof(TrackBarItem), typeof(UITypeEditor))]
        public float Theta
        {
            set 
            { 
                this.fTheta = value;
            }
            get { return this.fTheta; }
        }

        [CategoryAttribute("Orientación"), DescriptionAttribute("Establece ángulo Phi.")]
        public float Phi
        {
            set { this.fPhi = value; }
            get { return this.fPhi; }
        }

        [CategoryAttribute("Rendimiento"), DescriptionAttribute("Número de divisiones en la geometría del Skydome")]
        public int NumDivisiones
        {
            set { this.DomeN = Math.Max(value,6); Initialize();  }
            get { return this.DomeN; }
        }

        [CategoryAttribute("Rendimiento"), DescriptionAttribute("Tamaño horizontal de los RenderTargets")]
        public int TamH
        {
            set { this.texW = Math.Max(value, 16); Initialize(); UpdateMieRayleighTextures(); }
            get { return this.texW; }
        }

        [CategoryAttribute("Rendimiento"), DescriptionAttribute("Tamaño vertical de los RenderTargets")]
        public int TamV
        {
            set { this.texH = Math.Max(value, 8); Initialize(); UpdateMieRayleighTextures(); }
            get { return this.texH; }
        }

        [CategoryAttribute("Aspecto"),ReadOnlyAttribute(true), DescriptionAttribute("Dirección de la luz")]
        public Vector4 LightDirection { get { return lightDirection; }}

        //public Vector4 LightColor { get { return lightColor; } set { lightColor = value; } }

        //public Vector4 LightColorAmbient { get { return lightColorAmbient; } set { lightColorAmbient = value; } }

        //public float FogDensity { get { return fDensity; } set { fDensity = value; } }

        //public Vector3 InvWaveLengths
        //{
        //    get { return invWaveLengths; }
        //}

        //public Vector3 WaveLengthsMie
        //{
        //    get { return waveLengthsMie; }
        //}

        [CategoryAttribute("Aspecto"), DescriptionAttribute("Longitudes de onda para cada componente de color.")]
        public Vector3 WaveLengths
        {
            get { return waveLengths; }
            set
            {
                waveLengths = value;
                setLengths();
                UpdateMieRayleighTextures();
            }
        }

        [CategoryAttribute("Rendimiento"), DescriptionAttribute("Número de iteraciones en el cálculo del Scattering.")]
        public int NumIteraciones
        {
            get { return numSamples; }
            set { numSamples = value; UpdateMieRayleighTextures(); }
        }

        [CategoryAttribute("Aspecto"), DescriptionAttribute("Grado de exposición de la lente (HDR).")]
        public float Exposicion
        {
            get { return exposure; }
            set { exposure = value; UpdateMieRayleighTextures(); }
        }

        [Browsable(false)]
        public Dictionary<string, TrackBarProperty> TrackBarProperties
        {
            get { return trackBarProperties; }
        }

        [Browsable(false)]
        public Int64 Tag { get; set; }

        #endregion


        #region Constructor

        public SkydomeObject()
        {
            fTheta = 1.6f;
            fPhi = 0.0f;

            texW = 64;
            texH = 32;

            DomeN = 32;

            lightDirection = new Vector4(100.0f, 100.0f, 100.0f, 1.0f);
            lightColor = new Vector4(1.0f, 1.0f, 1.0f, 1.0f);
            lightColorAmbient = new Vector4(0.2f, 0.2f, 0.2f, 1.0f);
            //fDensity = 0.0003f;
            waveLengths = new Vector3(0.65f, 0.57f, 0.475f);
            numSamples = 10;
            exposure = -2.0f;

            setLengths();

            #region Editor TrackBarProperties
            trackBarProperties = new Dictionary<string, TrackBarProperty>();

            //Phi
            TrackBarProperty tbpTheta = new TrackBarProperty();
            tbpTheta.Min = 0;
            tbpTheta.Max = 80;
            tbpTheta.TickFrequency = 5;
            tbpTheta.Div = 10.0f;
            trackBarProperties.Add("Theta", tbpTheta);

            #endregion
        }

        #endregion


        #region Initialize

        public void Initialize()
        {
            graphics = Editor.Instance.Render.Device;
            content = Editor.Instance.Render.ContentManager;

            camera = Editor.Instance.Render.Camera;

            realTime = false;

            this.mieRT = new RenderTarget2D(graphics, texW, texH, 1,
                SurfaceFormat.HalfVector4, MultiSampleType.None, 0);
            this.rayleighRT = new RenderTarget2D(graphics, texW, texH, 1,
                SurfaceFormat.HalfVector4, MultiSampleType.None, 0);

            Load();
        }

        private void Load()
        {
            screenQuad = new SpriteBatch(graphics);

            scatterEffect = content.Load<Effect>("Effects/scatter");
            texturedEffect = content.Load<Effect>("Effects/Textured");

            oldDepthBuffer = graphics.DepthStencilBuffer;

            this.newDepthBuffer = new DepthStencilBuffer(graphics,
                graphics.PresentationParameters.BackBufferWidth,
                graphics.PresentationParameters.BackBufferHeight,
                graphics.DepthStencilBuffer.Format, MultiSampleType.None, 0);

            moonTex = content.Load<Texture2D>("Textures/moon");
            glowTex = content.Load<Texture2D>("Textures/moonglow");
            starsTex = content.Load<Texture2D>("Textures/starfield");

            dummy = new Texture2D(graphics, 1, 1, 1, TextureUsage.None, SurfaceFormat.Color);

            GenerateDome();
            GenerateMoon();
        }

        #endregion

        #region Update

        public void Update(GameTime gameTime)
        {
            KeyboardState keyState = Keyboard.GetState();

            if (realTime)
            {
                int minutos = DateTime.Now.Hour * 60 + DateTime.Now.Minute;
                this.fTheta = (float)minutos * (float)(Math.PI) / 12.0f / 60.0f;
            }

            this.lightDirection = this.GetDirection();
            //this.lightDirection.Normalize();
        }

        #endregion

        #region Draw

        public void Draw(GameTime gameTime)
        {
            Matrix View = camera.View;
            Matrix Projection = camera.Projection;
            Matrix World = Matrix.CreateTranslation(camera.Position.X,
                camera.Position.Y,
                camera.Position.Z);

            if (previousTheta != fTheta || previousPhi != fPhi)
                UpdateMieRayleighTextures();

            this.sunColor = this.GetSunColor(-this.fTheta, 2);

            graphics.Clear(Color.CornflowerBlue);

            graphics.RenderState.DepthBufferEnable = false;
            graphics.RenderState.DepthBufferWriteEnable = false;
            graphics.RenderState.CullMode = CullMode.None;

            scatterEffect.CurrentTechnique = scatterEffect.Techniques["Render"];
            scatterEffect.Begin();
            scatterEffect.Parameters["txMie"].SetValue(this.mieTex);
            scatterEffect.Parameters["txRayleigh"].SetValue(this.rayleighTex);
            scatterEffect.Parameters["WorldViewProjection"].SetValue(World * View * Projection);
            //scatterEffect.Parameters["v3SunDir"].SetValue(new Vector3(-parameters.LightDirection.X,
            //    -parameters.LightDirection.Y, -parameters.LightDirection.Z));
            //scatterEffect.Parameters["NumSamples"].SetValue(parameters.NumSamples);
            scatterEffect.Parameters["fExposure"].SetValue(this.exposure);
            scatterEffect.Parameters["StarsTex"].SetValue(starsTex);
            if (fTheta < Math.PI / 2.0f || fTheta > 3.0f * Math.PI / 2.0f)
                scatterEffect.Parameters["starIntensity"].SetValue((float)Math.Abs(
                    Math.Sin(fTheta + (float)Math.PI / 2.0f)));
            else
                scatterEffect.Parameters["starIntensity"].SetValue(0.0f);

            foreach (EffectPass pass in scatterEffect.CurrentTechnique.Passes)
            {
                pass.Begin();

                graphics.VertexDeclaration = vertexDecl;
                graphics.DrawUserIndexedPrimitives<VertexPositionTexture>
                    (PrimitiveType.TriangleList, domeVerts, 0, this.DVSize, ib, 0, this.DISize);

                pass.End();
            }

            scatterEffect.End();

            DrawGlow();
            DrawMoon();

            graphics.RenderState.DepthBufferWriteEnable = true;
            graphics.RenderState.DepthBufferEnable = true;

            previousTheta = this.fTheta;
            previousPhi = this.fPhi;
        }

        #region DrawMoon

        private void DrawMoon()
        {
            graphics.RenderState.AlphaBlendEnable = true;
            graphics.RenderState.SourceBlend = Blend.SourceAlpha;
            graphics.RenderState.DestinationBlend = Blend.InverseSourceAlpha;

            texturedEffect.CurrentTechnique = texturedEffect.Techniques["Textured"];
            texturedEffect.Begin();
            texturedEffect.Parameters["World"].SetValue(
                Matrix.CreateRotationX(fTheta + (float)Math.PI / 2.0f) *
                Matrix.CreateRotationY(-fPhi + (float)Math.PI / 2.0f) *
                Matrix.CreateTranslation(this.lightDirection.X * 15,
                this.lightDirection.Y * 15,
                this.lightDirection.Z * 15) *
                Matrix.CreateTranslation(camera.Position.X,
                camera.Position.Y,
                camera.Position.Z));
            texturedEffect.Parameters["View"].SetValue(camera.View);
            texturedEffect.Parameters["Projection"].SetValue(camera.Projection);
            texturedEffect.Parameters["Texture"].SetValue(this.moonTex);
            if (fTheta < Math.PI / 2.0f || fTheta > 3.0f * Math.PI / 2.0f)
                texturedEffect.Parameters["alpha"].SetValue((float)Math.Abs(
                    Math.Sin(fTheta + (float)Math.PI / 2.0f)));
            else
                texturedEffect.Parameters["alpha"].SetValue(0.0f);
            foreach (EffectPass pass in texturedEffect.CurrentTechnique.Passes)
            {
                pass.Begin();

                graphics.DrawUserIndexedPrimitives<VertexPositionTexture>
                    (PrimitiveType.TriangleList, quadVerts, 0, 4, quadIb, 0, 2);

                pass.End();
            }
            texturedEffect.End();

            graphics.RenderState.AlphaBlendEnable = false;

        }

        #endregion

        #region DrawGlow

        private void DrawGlow()
        {
            graphics.RenderState.AlphaBlendEnable = true;
            graphics.RenderState.SourceBlend = Blend.SourceAlpha;
            graphics.RenderState.DestinationBlend = Blend.InverseSourceAlpha;

            texturedEffect.CurrentTechnique = texturedEffect.Techniques["Textured"];
            texturedEffect.Begin();
            texturedEffect.Parameters["World"].SetValue(
                Matrix.CreateRotationX(fTheta + (float)Math.PI / 2.0f) *
                Matrix.CreateRotationY(-fPhi + (float)Math.PI / 2.0f) *
                Matrix.CreateTranslation(this.lightDirection.X * 5,
                this.lightDirection.Y * 5,
                this.lightDirection.Z * 5) *
                Matrix.CreateTranslation(camera.Position.X,
                camera.Position.Y,
                camera.Position.Z));//*
            texturedEffect.Parameters["View"].SetValue(camera.View);
            texturedEffect.Parameters["Projection"].SetValue(camera.Projection);
            texturedEffect.Parameters["Texture"].SetValue(this.glowTex);
            if (fTheta < Math.PI / 2.0f || fTheta > 3.0f * Math.PI / 2.0f)
                texturedEffect.Parameters["alpha"].SetValue((float)Math.Abs(
                    Math.Sin(fTheta + (float)Math.PI / 2.0f)));
            else
                texturedEffect.Parameters["alpha"].SetValue(0.0f);
            foreach (EffectPass pass in texturedEffect.CurrentTechnique.Passes)
            {
                pass.Begin();

                graphics.DrawUserIndexedPrimitives<VertexPositionTexture>
                    (PrimitiveType.TriangleList, quadVerts, 0, 4, quadIb, 0, 2);

                pass.End();
            }
            texturedEffect.End();

            graphics.RenderState.AlphaBlendEnable = false;

        }

        #endregion

        #endregion


        #region Private Methods

        #region GenerateDome

        private void GenerateDome()
        {
            int Latitude = DomeN / 2;
            int Longitude = DomeN;
            DVSize = Longitude * Latitude;
            DISize = (Longitude - 1) * (Latitude - 1) * 2;
            DVSize *= 2;
            DISize *= 2;

            
            vertexDecl = new VertexDeclaration(graphics,
                                VertexPositionTexture.VertexElements);

            domeVerts = new VertexPositionTexture[DVSize];

            // Fill Vertex Buffer
            int DomeIndex = 0;
            for (int i = 0; i < Longitude; i++)
            {
                double MoveXZ = 100.0f * (i / ((float)Longitude - 1.0f)) * MathHelper.Pi / 180.0;

                for (int j = 0; j < Latitude; j++)
                {
                    double MoveY = MathHelper.Pi * j / (Latitude - 1);

                    domeVerts[DomeIndex] = new VertexPositionTexture();
                    domeVerts[DomeIndex].Position.X = (float)(Math.Sin(MoveXZ) * Math.Cos(MoveY));
                    domeVerts[DomeIndex].Position.Y = (float)Math.Cos(MoveXZ);
                    domeVerts[DomeIndex].Position.Z = (float)(Math.Sin(MoveXZ) * Math.Sin(MoveY));

                    domeVerts[DomeIndex].Position *= 10.0f;

                    domeVerts[DomeIndex].TextureCoordinate.X = 0.5f / (float)Longitude + i / (float)Longitude;
                    domeVerts[DomeIndex].TextureCoordinate.Y = 0.5f / (float)Latitude + j / (float)Latitude;

                    DomeIndex++;
                }
            }
            for (int i = 0; i < Longitude; i++)
            {
                double MoveXZ = 100.0 * (i / (float)(Longitude - 1)) * MathHelper.Pi / 180.0;

                for (int j = 0; j < Latitude; j++)
                {
                    double MoveY = (MathHelper.Pi * 2.0) - (MathHelper.Pi * j / (Latitude - 1));

                    domeVerts[DomeIndex] = new VertexPositionTexture();
                    domeVerts[DomeIndex].Position.X = (float)(Math.Sin(MoveXZ) * Math.Cos(MoveY));
                    domeVerts[DomeIndex].Position.Y = (float)Math.Cos(MoveXZ);
                    domeVerts[DomeIndex].Position.Z = (float)(Math.Sin(MoveXZ) * Math.Sin(MoveY));

                    domeVerts[DomeIndex].Position *= 10.0f;

                    domeVerts[DomeIndex].TextureCoordinate.X = 0.5f / (float)Longitude + i / (float)Longitude;
                    domeVerts[DomeIndex].TextureCoordinate.Y = 0.5f / (float)Latitude + j / (float)Latitude;

                    DomeIndex++;
                }
            }

            // Fill index buffer
            ib = new short[DISize * 3];
            int index = 0;
            for (short i = 0; i < Longitude - 1; i++)
            {
                for (short j = 0; j < Latitude - 1; j++)
                {
                    ib[index++] = (short)(i * Latitude + j);
                    ib[index++] = (short)((i + 1) * Latitude + j);
                    ib[index++] = (short)((i + 1) * Latitude + j + 1);

                    ib[index++] = (short)((i + 1) * Latitude + j + 1);
                    ib[index++] = (short)(i * Latitude + j + 1);
                    ib[index++] = (short)(i * Latitude + j);
                }
            }
            short Offset = (short)(Latitude * Longitude);
            for (short i = 0; i < Longitude - 1; i++)
            {
                for (short j = 0; j < Latitude - 1; j++)
                {
                    ib[index++] = (short)(Offset + i * Latitude + j);
                    ib[index++] = (short)(Offset + (i + 1) * Latitude + j + 1);
                    ib[index++] = (short)(Offset + (i + 1) * Latitude + j);

                    ib[index++] = (short)(Offset + i * Latitude + j + 1);
                    ib[index++] = (short)(Offset + (i + 1) * Latitude + j + 1);
                    ib[index++] = (short)(Offset + i * Latitude + j);
                }
            }
        }

        #endregion

        #region GenerateMoon

        private void GenerateMoon()
        {
            quadVerts = new VertexPositionTexture[]
                        {
                            new VertexPositionTexture(
                                new Vector3(1,-1,0),
                                new Vector2(1,1)),
                            new VertexPositionTexture(
                                new Vector3(-1,-1,0),
                                new Vector2(0,1)),
                            new VertexPositionTexture(
                                new Vector3(-1,1,0),
                                new Vector2(0,0)),
                            new VertexPositionTexture(
                                new Vector3(1,1,0),
                                new Vector2(1,0))
                        };

            quadIb = new short[] { 0, 1, 2, 2, 3, 0 };
        }

        #endregion

        #region GetSunColor

        Vector4 GetSunColor(float fTheta, int nTurbidity)
        {
            float fBeta = 0.04608365822050f * nTurbidity - 0.04586025928522f;
            float fTauR, fTauA;
            float[] fTau = new float[3];

            float coseno = (float)Math.Cos((double)fTheta + Math.PI);
            double factor = (double)fTheta / Math.PI * 180.0;
            double jarl = Math.Pow(93.885 - factor, -1.253);
            float potencia = (float)jarl;
            float m = 1.0f / (coseno + 0.15f * potencia);

            int i;
            float[] fLambda = new float[3];
            fLambda[0] = this.waveLengths.X;
            fLambda[1] = this.waveLengths.Y;
            fLambda[2] = this.waveLengths.Z;


            for (i = 0; i < 3; i++)
            {
                potencia = (float)Math.Pow((double)fLambda[i], 4.0);
                fTauR = (float)Math.Exp((double)(-m * 0.008735f * potencia));

                const float fAlpha = 1.3f;
                potencia = (float)Math.Pow((double)fLambda[i], (double)-fAlpha);
                if (m < 0.0f)
                    fTau[i] = 0.0f;
                else
                {
                    fTauA = (float)Math.Exp((double)(-m * fBeta * potencia));
                    fTau[i] = fTauR * fTauA;
                }

            }

            Vector4 vAttenuation = new Vector4(fTau[0], fTau[1], fTau[2], 1.0f);
            return vAttenuation;
        }

        #endregion

        #region Get Light Direction
        Vector4 GetDirection()
        {

            float y = (float)Math.Cos((double)this.fTheta);
            float x = (float)(Math.Sin((double)this.fTheta) * Math.Cos(this.fPhi));
            float z = (float)(Math.Sin((double)this.fTheta) * Math.Sin(this.fPhi));
            float w = 1.0f;

            return new Vector4(x, y, z, w);
        }
        #endregion

        #region UpdateMieRayleighTextures

        void UpdateMieRayleighTextures()
        {
            graphics.DepthStencilBuffer = newDepthBuffer;


            graphics.SetRenderTarget(0, rayleighRT);
            graphics.SetRenderTarget(1, mieRT);

            graphics.Clear(Color.CornflowerBlue);

            screenQuad.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.SaveState);
            scatterEffect.CurrentTechnique = scatterEffect.Techniques["Update"];
            scatterEffect.Begin();
            scatterEffect.Parameters["InvWavelength"].SetValue(this.invWaveLengths);
            scatterEffect.Parameters["WavelengthMie"].SetValue(this.waveLengthsMie);
            scatterEffect.Parameters["NumSamples"].SetValue(this.numSamples);
            scatterEffect.Parameters["v3SunDir"].SetValue(new Vector3(-this.lightDirection.X,
                -this.lightDirection.Y, -this.lightDirection.Z));
            scatterEffect.Parameters["InvOpticalDepthNLessOne"].SetValue(1.0f / (float)(texW-1));
            scatterEffect.Parameters["InvOpticalDepthN"].SetValue(1.0f / (float)texW);
            scatterEffect.Parameters["InvRayleighMieNLessOne"].SetValue(
                new Vector2(1.0f / (float)(texW - 1), 1.0f / (float)(texH - 1)));
            EffectPass pass = scatterEffect.CurrentTechnique.Passes[0];
            pass.Begin();
            screenQuad.Draw(dummy, new Rectangle(0, 0, texW, texH), Color.White);
            pass.End();
            scatterEffect.End();

            screenQuad.End();

            graphics.SetRenderTarget(1, null);
            graphics.SetRenderTarget(0, null);

            this.mieTex = mieRT.GetTexture();
            this.rayleighTex = rayleighRT.GetTexture();

            graphics.DepthStencilBuffer = oldDepthBuffer;

            //mieTex.Save("mimie.dds", ImageFileFormat.Dds);
            //rayleighTex.Save("mirayleigh.dds", ImageFileFormat.Dds);
        }

        #endregion

        #region SetLengths

        private void setLengths()
        {
            invWaveLengths.X = 1.0f / (float)Math.Pow((double)waveLengths.X, 4.0);
            invWaveLengths.Y = 1.0f / (float)Math.Pow((double)waveLengths.Y, 4.0);
            invWaveLengths.Z = 1.0f / (float)Math.Pow((double)waveLengths.Z, 4.0);

            waveLengthsMie.X = (float)Math.Pow((double)waveLengths.X, -0.84);
            waveLengthsMie.Y = (float)Math.Pow((double)waveLengths.Y, -0.84);
            waveLengthsMie.Z = (float)Math.Pow((double)waveLengths.Z, -0.84);
        }

        #endregion

        #endregion
    }
}
